/*
 * Copyright (C) 2010-2011 Graz University of Technology, Austria. All Rights reserved. 
 *
 * Contact: Simone Kopeinik <simone.kopeinik@tugraz.at>
 * 	   Graz University of Technology, Knowledge Management Institute, Brückenkopfgasse 1/6, 8020 Graz 	
 * 	   <http://www.kmi.tugraz.at/>
 * 
 * This software is part of the TARGET platform developed by the
 * TARGET EC-funded project www.reachyourtarget.org
 *
 * This software is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * The GNU General Public License is available at <http://www.gnu.org/licenses/>.
 *
 */
package at.tugraz.kmi.css.cbkst2.prereq.structurevis;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Point2D;

import javax.swing.JComponent;

import org.apache.commons.collections15.Factory;
import org.apache.commons.collections15.Transformer;
import org.apache.commons.collections15.functors.ConstantTransformer;

import at.tugraz.kmi.css.cbkst2.CbKSTVertex;
import at.tugraz.kmi.css.cbkst2.PrerequesiteRelation;
import at.tugraz.kmi.css.cbkst2.prereq.PrerequesiteOrderSetGraph;
import edu.uci.ics.jung.algorithms.layout.GraphElementAccessor;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.graph.DirectedGraph;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.UndirectedGraph;
import edu.uci.ics.jung.graph.util.EdgeType;
import edu.uci.ics.jung.visualization.VisualizationServer;
import edu.uci.ics.jung.visualization.VisualizationViewer;
import edu.uci.ics.jung.visualization.control.AbstractGraphMousePlugin;
import edu.uci.ics.jung.visualization.util.ArrowFactory;

/**
 * A plugin that can create vertices, undirected edges, and directed edges using
 * mouse gestures.
 * 
 * @author Tom Nelson
 * 
 */
final class CbKSTEditingGraphMousePlugin<V extends CbKSTVertex, E> extends
		AbstractGraphMousePlugin implements MouseListener, MouseMotionListener {

	protected V								startVertex;

	protected CubicCurve2D					rawEdge		= new CubicCurve2D.Float();
	protected Shape							edgeShape;
	protected Shape							rawArrowShape;
	protected Shape							arrowShape;
	protected VisualizationServer.Paintable	edgePaintable;
	protected VisualizationServer.Paintable	arrowPaintable;
	protected EdgeType						edgeIsDirected;
	protected Factory<V>					vertexFactory;
	protected Factory<E>					edgeFactory;
	Boolean									editable	= true;

	CbKSTEditingGraphMousePlugin(Factory<V> vertexFactory,
			Factory<E> edgeFactory) {
		this(InputEvent.BUTTON1_MASK, vertexFactory, edgeFactory);
	}

	/**
	 * create instance and prepare shapes for visual effects
	 * 
	 * @param modifiers
	 */
	private CbKSTEditingGraphMousePlugin(int modifiers,
			Factory<V> vertexFactory, Factory<E> edgeFactory) {
		super(modifiers);
		this.vertexFactory = vertexFactory;
		this.edgeFactory = edgeFactory;
		rawEdge.setCurve(0.0f, 0.0f, 0.33f, 100, .66f, -50, 1.0f, 0.0f);
		rawArrowShape = ArrowFactory.getNotchedArrow(20, 16, 8);
		edgePaintable = new EdgePaintable();
		arrowPaintable = new ArrowPaintable();
		cursor = Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
	}

	/**
	 * Overridden to be more flexible, and pass events with key combinations.
	 * The default responds to both ButtonOne and ButtonOne+Shift
	 */
	@Override
	public boolean checkModifiers(MouseEvent e) {
		return (e.getModifiers() & modifiers) != 0;
	}

	/**
	 * If the mouse is pressed in an empty area, create a new vertex there. If
	 * the mouse is pressed on an existing vertex, prepare to create an edge
	 * from that vertex to another
	 */
	@Override
	@SuppressWarnings("unchecked")
	public void mousePressed(MouseEvent e) {
		// if (e.isControlDown()) {
		if (checkModifiers(e)) {
			final VisualizationViewer<V, E> vv = (VisualizationViewer<V, E>) e
					.getSource();
			final Point2D p = e.getPoint();
			GraphElementAccessor<V, E> pickSupport = vv.getPickSupport();
			if (pickSupport != null) {
				Graph<V, E> graph = vv.getModel().getGraphLayout().getGraph();
				// set default edge type
				if (graph instanceof DirectedGraph) {
					edgeIsDirected = EdgeType.DIRECTED;
				} else {
					edgeIsDirected = EdgeType.UNDIRECTED;
				}

				final V vertex = pickSupport.getVertex(vv.getModel()
						.getGraphLayout(), p.getX(), p.getY());
				if (vertex != null) { // get ready to make an edge
					startVertex = vertex;
					down = e.getPoint();

					if (graph instanceof PrerequesiteOrderSetGraph) {
						PrerequesiteOrderSetGraph jungGraph = (PrerequesiteOrderSetGraph) graph;
						vv.getRenderContext().setVertexFillPaintTransformer(
								new ConnectedVerticesPainter(jungGraph,
										Color.orange, Color.red));
					}
					transformEdgeShape(down, down);
					vv.addPostRenderPaintable(edgePaintable);
					if ((e.getModifiers() & InputEvent.SHIFT_MASK) != 0
							&& vv.getModel().getGraphLayout().getGraph() instanceof UndirectedGraph == false) {
						edgeIsDirected = EdgeType.DIRECTED;
					}
					if (edgeIsDirected == EdgeType.DIRECTED) {
						transformArrowShape(down, e.getPoint());
						vv.addPostRenderPaintable(arrowPaintable);
					}
				} else { // make a new vertex
					if (editable) {
						V newVertex = vertexFactory.create();
						Layout<V, E> layout = vv.getModel().getGraphLayout();
						graph.addVertex(newVertex);
						layout.setLocation(newVertex,
								vv.getRenderContext()
										.getMultiLayerTransformer()
										.inverseTransform(e.getPoint()));
					}

				}
			}
			vv.repaint();
		}
		// }
	}

	private final class ConnectedVerticesPainter implements
			Transformer<V, Paint> {

		private final PrerequesiteOrderSetGraph<V, PrerequesiteRelation>	graph;
		private final Color													active;
		private final Color													inactive;

		public ConnectedVerticesPainter(
				PrerequesiteOrderSetGraph<V, PrerequesiteRelation> graph,
				Color active, Color inactive) {
			this.active = active;
			this.inactive = inactive;
			this.graph = graph;
		}

		@Override
		public Paint transform(V input) {
			PrerequesiteRelation pathDescendants = this.graph
					.getTransitiveClosureGraph().findEdge(
							CbKSTEditingGraphMousePlugin.this.startVertex,
							input);
			PrerequesiteRelation pathAscendants = this.graph
					.getTransitiveClosureGraph().findEdge(input,
							CbKSTEditingGraphMousePlugin.this.startVertex);
			if ((pathDescendants != null || pathAscendants != null)) {
				// Passive
				return inactive;
			} else {
				// Active
				return active;
			}
		}
	}

	/**
	 * If startVertex is non-null, and the mouse is released over an existing
	 * vertex, create an undirected edge from startVertex to the vertex under
	 * the mouse pointer. If shift was also pressed, create a directed edge
	 * instead.
	 */
	@Override
	@SuppressWarnings("unchecked")
	public void mouseReleased(MouseEvent e) {
		if (checkModifiers(e)) {
			final VisualizationViewer<V, E> vv = (VisualizationViewer<V, E>) e
					.getSource();
			final Point2D p = e.getPoint();
			Layout<V, E> layout = vv.getModel().getGraphLayout();
			GraphElementAccessor<V, E> pickSupport = vv.getPickSupport();
			if (pickSupport != null) {
				final V vertex = pickSupport.getVertex(layout, p.getX(),
						p.getY());
				if (vertex != null && startVertex != null) {
					Graph<V, E> graph = vv.getGraphLayout().getGraph();
					graph.addEdge(edgeFactory.create(), startVertex, vertex,
							edgeIsDirected);
					vv.repaint();
				}
			}
			startVertex = null;
			down = null;
			edgeIsDirected = EdgeType.UNDIRECTED;
			vv.removePostRenderPaintable(edgePaintable);
			vv.removePostRenderPaintable(arrowPaintable);
			Graph<V, E> graph = vv.getGraphLayout().getGraph();
			if (graph instanceof PrerequesiteOrderSetGraph) {
				Transformer<V, Paint> vertexFillPaintTransformer = new ConstantTransformer(
						Color.RED);
				vv.getRenderContext().setVertexFillPaintTransformer(
						vertexFillPaintTransformer);
			}
		}
	}

	/**
	 * If startVertex is non-null, stretch an edge shape between startVertex and
	 * the mouse pointer to simulate edge creation
	 */
	@Override
	@SuppressWarnings("unchecked")
	public void mouseDragged(MouseEvent e) {
		if (checkModifiers(e)) {
			if (startVertex != null) {
				transformEdgeShape(down, e.getPoint());
				if (edgeIsDirected == EdgeType.DIRECTED) {
					transformArrowShape(down, e.getPoint());
				}
			}
			VisualizationViewer<V, E> vv = (VisualizationViewer<V, E>) e
					.getSource();
			vv.repaint();
		}
	}

	/**
	 * code lifted from PluggableRenderer to move an edge shape into an
	 * arbitrary position
	 */
	private void transformEdgeShape(Point2D down, Point2D out) {
		float x1 = (float) down.getX();
		float y1 = (float) down.getY();
		float x2 = (float) out.getX();
		float y2 = (float) out.getY();

		AffineTransform xform = AffineTransform.getTranslateInstance(x1, y1);

		float dx = x2 - x1;
		float dy = y2 - y1;
		float thetaRadians = (float) Math.atan2(dy, dx);
		xform.rotate(thetaRadians);
		float dist = (float) Math.sqrt(dx * dx + dy * dy);
		xform.scale(dist / rawEdge.getBounds().getWidth(), 1.0);
		edgeShape = xform.createTransformedShape(rawEdge);
	}

	private void transformArrowShape(Point2D down, Point2D out) {
		float x1 = (float) down.getX();
		float y1 = (float) down.getY();
		float x2 = (float) out.getX();
		float y2 = (float) out.getY();

		AffineTransform xform = AffineTransform.getTranslateInstance(x2, y2);

		float dx = x2 - x1;
		float dy = y2 - y1;
		float thetaRadians = (float) Math.atan2(dy, dx);
		xform.rotate(thetaRadians);
		arrowShape = xform.createTransformedShape(rawArrowShape);
	}

	/**
	 * Used for the edge creation visual effect during mouse drag
	 */
	class EdgePaintable implements VisualizationServer.Paintable {

		@Override
		public void paint(Graphics g) {
			if (edgeShape != null) {
				Color oldColor = g.getColor();
				g.setColor(Color.black);
				((Graphics2D) g).draw(edgeShape);
				g.setColor(oldColor);
			}
		}

		@Override
		public boolean useTransform() {
			return false;
		}
	}

	/**
	 * Used for the directed edge creation visual effect during mouse drag
	 */
	class ArrowPaintable implements VisualizationServer.Paintable {

		@Override
		public void paint(Graphics g) {
			if (arrowShape != null) {
				Color oldColor = g.getColor();
				g.setColor(Color.black);
				((Graphics2D) g).fill(arrowShape);
				g.setColor(oldColor);
			}
		}

		@Override
		public boolean useTransform() {
			return false;
		}
	}

	@Override
	public void mouseClicked(MouseEvent e) {
	}

	@Override
	public void mouseEntered(MouseEvent e) {
		JComponent c = (JComponent) e.getSource();
		c.setCursor(cursor);
	}

	@Override
	public void mouseExited(MouseEvent e) {
		JComponent c = (JComponent) e.getSource();
		c.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
	}

	@Override
	public void mouseMoved(MouseEvent e) {
	}
}
